package token_logic

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/dgrijalva/jwt-go"
	"github.com/pkg/errors"
	"github.com/spf13/viper"
	"time"
	"wx-uniapp-go/internal/domain/errcode"
	models "wx-uniapp-go/internal/repository/models"
	"wx-uniapp-go/third_party/errs"
	"wx-uniapp-go/third_party/rediscli"
)

var (
	ErrOtherDeviceLogin = errs.NewBusinessErr(errcode.Unauthorized.Code, "当前账号已于其他设备登录")
)

const (
	appIdXIAODAMini = "XIAODA"
)

type tokenClient struct {
	Ctx context.Context
}

func NewTokenClient(ctx context.Context) *tokenClient {
	return &tokenClient{Ctx: ctx}
}

func (s *tokenClient) CreateCommonToken(appId string, userId int64) (string, int64, error) {

	authSecret := viper.GetString("encrypt.auth_secret") //token密钥
	exp := viper.GetInt64("encrypt.token_expire")

	expireTime := time.Now().Unix() + exp
	uidStr := models.UidEncode(userId)
	user := new(models.User)
	_, err := models.Get(s.Ctx, user, "id = ?", userId)
	if err != nil {
		return "", 0, err
	}

	appToken, err := s.createToken(appId, uidStr, user, exp, authSecret)
	if err != nil {
		//token创建失败
		return "", expireTime, err
	}

	// 小程序登陆不参与单点登录
	if appId == appIdXIAODAMini {
		return appToken, expireTime, nil
	}

	rediscli.Default().Set(s.Ctx, s.getRedisKey(userId), appToken, time.Duration(exp)*time.Second)

	return appToken, expireTime, nil
}

type CommonToken struct {
	Id     any
	AppId  any
	Expire int64
}

// createToken 为了通用性，appid和uid设置为interface类型，expire 是过期时间，不是过期时间戳，见单元测试
func (s *tokenClient) createToken(appId, uid, uniqueId interface{}, expire int64, authSecret string) (string, error) {
	if expire == 0 {
		expire = 86400
	}
	timeNow := time.Now().Unix()
	expireTime := timeNow + expire
	authCode := []byte(authSecret)
	claims := jwt.MapClaims{
		"appid":  appId,
		"id":     uid,
		"kid":    uniqueId,
		"expire": expireTime,
		"iat":    timeNow, //签发时间  可以确保每次登录得到的token不同
	}
	tokenData := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	authToken, err := tokenData.SignedString(authCode)
	return authToken, err
}

func (s *tokenClient) CheckCommonToken(tokenStr string) (*CommonToken, error) {
	authSecret := viper.GetString("encrypt.auth_secret")
	tokenData, err := s.checkToken(tokenStr, authSecret)
	if err != nil {
		return nil, err
	}

	return tokenData, nil
}

func (s *tokenClient) checkToken(tokenString, authSecret string, notChecks ...bool) (*CommonToken, error) {
	if tokenString == "" {
		return nil, errors.New("invalid token")
	}
	tokenData, err := jwt.ParseWithClaims(tokenString, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte(authSecret), nil
	})
	if tokenData == nil {
		return nil, errors.New("invalid token")
	}
	if err != nil {
		return nil, errors.Wrap(err, "invalid token")
	}
	if claims, ok := tokenData.Claims.(jwt.MapClaims); ok && tokenData.Valid {
		jsonstr, err0 := json.Marshal(claims)
		tokenStruct := &CommonToken{}
		err0 = json.Unmarshal(jsonstr, tokenStruct)
		if err0 != nil {
			return nil, errors.New("invalid token")
		}
		notCheck := false
		if len(notChecks) > 0 {
			notCheck = notChecks[0]
		}
		if notCheck == false {
			expireTime := tokenStruct.Expire
			if time.Now().Unix() > expireTime {
				return nil, errors.New("token expire")
			}
		}
		return tokenStruct, nil
	}
	return nil, errors.New("invalid token")
}

func (s *tokenClient) checkDeviceNum(id any, tokenStr string) error {

	uid := int64(0)
	if uidStr, ok := id.(string); !ok {
		return nil
	} else {
		uid = models.UidDecode(uidStr)
	}

	usingToken, err := rediscli.Default().Get(s.Ctx, s.getRedisKey(uid))
	if err != nil {
		return err
	}
	if usingToken != tokenStr {
		return ErrOtherDeviceLogin
	}
	return nil
}

func (s *tokenClient) getRedisKey(uid int64) string {
	return fmt.Sprintf("user:token:%d", uid)
}
